diff options
Diffstat (limited to 'app/api/auth/[...nextauth]/saml')
| -rw-r--r-- | app/api/auth/[...nextauth]/saml/provider.ts | 259 | ||||
| -rw-r--r-- | app/api/auth/[...nextauth]/saml/utils.ts | 485 |
2 files changed, 744 insertions, 0 deletions
diff --git a/app/api/auth/[...nextauth]/saml/provider.ts b/app/api/auth/[...nextauth]/saml/provider.ts new file mode 100644 index 00000000..8486a690 --- /dev/null +++ b/app/api/auth/[...nextauth]/saml/provider.ts @@ -0,0 +1,259 @@ +import CredentialsProvider from "next-auth/providers/credentials" +import { getOrCreateSAMLUser, validateSAMLUserData } from '@/lib/users/saml-service' +import { encode } from 'next-auth/jwt' +import type { User } from 'next-auth' +import type { SAMLUser } from './utils' +import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils' + +interface SAMLProviderOptions { + id: string + name: string + idp: { + sso_login_url: string + sso_logout_url: string + certificates: string[] + } + sp: { + entity_id: string + private_key: string + certificate: string + assert_endpoint: string + } +} + +export function SAMLProvider(options: SAMLProviderOptions) { + return CredentialsProvider({ + id: options.id, + name: options.name, + credentials: { + user: { + label: "User Data", + type: "text" + } + }, + async authorize(credentials) { + debugLog('π SAMLProvider.authorize called with credentials:', credentials); + + try { + debugLog('π Checking credentials.user:', { + hasCredentials: !!credentials, + hasUser: !!credentials?.user, + userType: typeof credentials?.user, + userValue: credentials?.user?.substring?.(0, 100) + '...' + }); + + if (!credentials?.user) { + debugError('No user data provided in credentials') + return null + } + + debugProcess('SAML Provider: Processing user data') + + // μ¬μ©μ λ°μ΄ν° νμ± (UTF-8 μ²λ¦¬ κ°μ ) + const userDataString = credentials.user + debugLog('π€ Raw user data string:', userDataString.substring(0, 200) + '...') + + let userData; + try { + userData = JSON.parse(userDataString); + debugSuccess('JSON parsing successful:', userData); + } catch (parseError) { + debugError('JSON parsing failed:', parseError); + debugError('Raw string that failed to parse:', userDataString); + return null; + } + + // νμ±λ λ°μ΄ν°μ UTF-8 νμΈ + debugLog('π€ Parsed user data UTF-8 check:', { + name: userData.name, + nameLength: userData.name?.length, + charCodes: userData.name ? [...userData.name].map(c => c.charCodeAt(0)) : [] + }) + + if (!userData.id || !userData.email) { + debugError('Invalid SAML user data:', userData) + return null + } + + debugSuccess('SAML Provider: User authenticated successfully', { + id: userData.id, + email: userData.email, + name: userData.name + }) + + // π₯ SAML μ¬μ©μ λ°μ΄ν° κ²μ¦ + debugProcess('Validating SAML user data structure...'); + const isValidData = await validateSAMLUserData(userData) + debugLog('Validation result:', isValidData); + if (!isValidData) { + debugError('Invalid SAML user data structure:', userData) + return null + } + + // π₯ JIT (Just-In-Time) μ¬μ©μ μμ± λλ μ‘°ν + debugProcess('Creating/getting SAML user from database...'); + const userCreateData = { + email: userData.email, + name: userData.name, + companyId: undefined, + techCompanyId: undefined, + domain: userData.domain + }; + debugLog('User create data:', userCreateData); + + const dbUser = await getOrCreateSAMLUser(userCreateData) + debugLog('Database user result:', dbUser); + + if (!dbUser) { + debugError('Failed to get or create SAML user') + return null + } + + // DBμμ κ°μ Έμ¨ μ€μ μ¬μ©μ μ 보 λ°ν + const userResult = { + id: String(dbUser.id), // DBμ μ€μ ID + name: dbUser.name, // DBμ μ€μ μ΄λ¦ + email: dbUser.email, // DBμ μ€μ μ΄λ©μΌ + companyId: dbUser.companyId, // DBμ μ€μ νμ¬ ID + techCompanyId: dbUser.techCompanyId, // DBμ μ€μ κΈ°μ νμ¬ ID + domain: dbUser.domain, // DBμ μ€μ λλ©μΈ + imageUrl: dbUser.imageUrl, // DBμ μ€μ μ΄λ―Έμ§ URL + } + + debugSuccess('SAML Provider: Returning user data to NextAuth:', userResult) + return userResult + } catch (error) { + debugError('SAML Provider: Authentication failed', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + errorType: typeof error, + credentials: credentials + }); + return null + } + } + }) +} + +// SAML λ‘κ·ΈμΈ URL μμ± ν¬νΌ ν¨μ +export function getSAMLLoginUrl(options: SAMLProviderOptions): string { + const params = new URLSearchParams({ + SAMLRequest: 'placeholder', // μ€μ λ‘λ createAuthnRequest()λ‘ μμ± + RelayState: options.sp.assert_endpoint, + }) + + return `${options.idp.sso_login_url}?${params.toString()}` +} + +// SAML μ€μ κ²μ¦ +export function validateSAMLOptions(options: SAMLProviderOptions): boolean { + const required = [ + options.idp.sso_login_url, + options.sp.entity_id, + options.sp.assert_endpoint + ] + + return required.every(field => field && field.length > 0) +} + +// SAMLProviderμ authorize ν¨μλ₯Ό μ§μ νΈμΆνκΈ° μν ν¬νΌ +export async function authenticateSAMLUser(userData: SAMLUser) { + debugLog('authenticateSAMLUser called with:', userData); + + try { + // SAMLProvider λμ μ§μ λ‘μ§ μ€ν (Provider λνΌ μμ΄) + debugProcess('SAML User Authentication: Processing user data') + + // μ¬μ©μ λ°μ΄ν° κ²μ¦ + if (!userData.id || !userData.email) { + debugError('Invalid SAML user data:', userData) + return null + } + + debugSuccess('SAML User data validated successfully', { + id: userData.id, + email: userData.email, + name: userData.name + }) + + // π₯ SAML μ¬μ©μ λ°μ΄ν° κ²μ¦ + debugLog('Validating SAML user data structure...'); + const isValidData = await validateSAMLUserData(userData) + debugLog('Validation result:', isValidData); + if (!isValidData) { + debugError('Invalid SAML user data structure:', userData) + return null + } + + // π₯ JIT (Just-In-Time) μ¬μ©μ μμ± λλ μ‘°ν + debugLog('Creating/getting SAML user from database...'); + const userCreateData = { + email: userData.email, + name: userData.name, + companyId: undefined, + techCompanyId: undefined, + domain: userData.domain + }; + debugLog('User create data:', userCreateData); + + const dbUser = await getOrCreateSAMLUser(userCreateData) + debugLog('Database user result:', dbUser); + + if (!dbUser) { + debugError('Failed to get or create SAML user') + return null + } + + // DBμμ κ°μ Έμ¨ μ€μ μ¬μ©μ μ 보 λ°ν + const userResult = { + id: String(dbUser.id), // DBμ μ€μ ID + name: dbUser.name, // DBμ μ€μ μ΄λ¦ + email: dbUser.email, // DBμ μ€μ μ΄λ©μΌ + companyId: dbUser.companyId, // DBμ μ€μ νμ¬ ID + techCompanyId: dbUser.techCompanyId, // DBμ μ€μ κΈ°μ νμ¬ ID + domain: dbUser.domain, // DBμ μ€μ λλ©μΈ + imageUrl: dbUser.imageUrl, // DBμ μ€μ μ΄λ―Έμ§ URL + } + + debugSuccess('SAML User Authentication completed:', userResult) + return userResult; + + } catch (error) { + debugError('authenticateSAMLUser error:', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + userData + }); + return null; + } +} + +// NextAuth JWT ν ν° μμ± ν¬νΌ +export async function createNextAuthToken(user: User): Promise<string> { + const token = { + id: user.id, + email: user.email, + name: user.name, + companyId: user.companyId, + techCompanyId: user.techCompanyId, + domain: user.domain, + imageUrl: user.imageUrl, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30μΌ + }; + + const secret = process.env.NEXTAUTH_SECRET!; + return await encode({ token, secret }); +} + +// NextAuth μΈμ
μΏ ν€ μ΄λ¦ κ°μ Έμ€κΈ° +export function getSessionCookieName(): string { + // NEXTAUTH_URLμ΄ HTTPSμΈ κ²½μ°μλ§ __Secure- μ λμ¬ μ¬μ© + const nextAuthUrl = process.env.NEXTAUTH_URL || ''; + const isHttps = nextAuthUrl.startsWith('https://'); + + return isHttps + ? '__Secure-next-auth.session-token' + : 'next-auth.session-token'; +} +
\ No newline at end of file diff --git a/app/api/auth/[...nextauth]/saml/utils.ts b/app/api/auth/[...nextauth]/saml/utils.ts new file mode 100644 index 00000000..a5bcfe7a --- /dev/null +++ b/app/api/auth/[...nextauth]/saml/utils.ts @@ -0,0 +1,485 @@ +import { SAML, ValidateInResponseTo } from "@node-saml/node-saml"; +import { + getIDPMetadata, + normalizeCertificate, +} from "@/lib/saml/idp-metadata"; +import { + getSPMetadata, +} from "@/lib/saml/sp-metadata"; +import { debugLog, debugError, debugSuccess, debugProcess, debugMock } from '@/lib/debug-utils'; + +export interface SAMLProfile { + nameID?: string; + nameIDFormat?: string; + attributes?: Record<string, string | string[]>; // λ¬Έμμ΄ λλ λ°°μ΄ λͺ¨λ μ§μ + [key: string]: unknown; +} + +export interface SAMLUser { + id: string; + email: string; + name: string; + companyId?: number; + techCompanyId?: number; + domain?: string; +} + +// SAML μ€μ μμ± (sync ν¨μ) - νκ²½λ³μ κΈ°λ°μΌλ‘ λ³κ²½νμ +export function createSAMLConfig() { + console.log("βοΈ Creating SAML configuration..."); + + try { + const idpMetadata = getIDPMetadata(); + const spMetadata = getSPMetadata(); + + console.log("π IdP Metadata loaded:", { + entityId: idpMetadata.entityId, + ssoUrl: idpMetadata.ssoUrl, + organization: idpMetadata.organization, + wantAuthnRequestsSigned: idpMetadata.wantAuthnRequestsSigned, + }); + + console.log("π SP Metadata loaded:", { + entityId: spMetadata.entityId, + callbackUrl: spMetadata.callbackUrl, + authnRequestsSigned: spMetadata.authnRequestsSigned, + }); + + const config = { + callbackUrl: spMetadata.callbackUrl, + // IDP λ©νλ°μ΄ν° κΈ°λ° μ€μ + entryPoint: idpMetadata.ssoUrl, + // SP Entity ID + issuer: spMetadata.entityId, + // IDP μΈμ¦μ (μ κ·νλ PEM νμ) + idpCert: normalizeCertificate(idpMetadata.certificate), + privateKey: process.env.SAML_SP_PRIVATE_KEY, + // IdPμμ μꡬνλ μ€μ + identifierFormat: idpMetadata.nameIdFormat, + signatureAlgorithm: "sha256" as const, + digestAlgorithm: "sha256", + // SP λ©νλ°μ΄ν° μ€μ + decryptionPvk: process.env.SAML_SP_PRIVATE_KEY, + publicCert: process.env.SAML_SP_CERT, + // IdP λ©νλ°μ΄ν° κΈ°λ° μ€μ + wantAuthnResponseSigned: idpMetadata.wantAuthnRequestsSigned, + wantAssertionsSigned: spMetadata.wantAssertionsSigned, + validateInResponseTo: ValidateInResponseTo.never, + disableRequestedAuthnContext: true, + // HTTP-Redirect λ°μΈλ© μ€μ + authnRequestBinding: undefined, // HTTP-Redirect (GET) μ¬μ© (κΈ°λ³Έκ°) + skipRequestCompression: false, // Deflate μμΆ μ¬μ© + // μΆκ° 보μ μ€μ + acceptedClockSkewMs: 5000, // 5μ΄ ν΄λ μ°¨μ΄ νμ© + forceAuthn: false, + // IDP Entity ID μ€μ + idpIssuer: idpMetadata.entityId, + }; + + console.log("β
SAML Config created:", { + callbackUrl: config.callbackUrl, + entryPoint: config.entryPoint, + issuer: config.issuer, + idpIssuer: config.idpIssuer, + identifierFormat: config.identifierFormat, + hasIdpCert: !!config.idpCert, + hasPrivateKey: !!config.privateKey, + hasPublicCert: !!config.publicCert, + wantAuthnResponseSigned: config.wantAuthnResponseSigned, + wantAssertionsSigned: config.wantAssertionsSigned, + }); + + return config; + } catch (error) { + console.error("π₯ Failed to create SAML Config:", error); + throw error; + } +} + +// SAML AuthnRequest μμ± (μλ² μ‘μ
) +export async function createAuthnRequest(relayState?: string): Promise<string> { + "use server"; + + console.log("SSO STEP 2: Create AuthnRequest", { relayState }); + + // Mock IdP λͺ¨λ μ²΄ν¬ + if (process.env.SAML_MOCKING_IDP === 'true') { + debugMock("Mock IdP mode enabled - simulating SAML response"); + return createMockSAMLFlow(relayState); + } + + try { + const config = createSAMLConfig(); + console.log("SAML Config ready for AuthnRequest generation"); + + const saml = new SAML(config); + console.log("SAML instance created, generating authorize URL..."); + + const startTime = Date.now(); + const authorizeUrl = await saml.getAuthorizeUrlAsync( + relayState || "", // RelayState - μλ κ°λ €λ νμ΄μ§ + undefined, // host + { + additionalParams: {}, + // additionalAuthorizeParams: {}, + } + ); + const endTime = Date.now(); + + // π SAML AuthnRequest λμ½λ© λ° λΆμ + try { + const urlObj = new URL(authorizeUrl); + const samlRequest = urlObj.searchParams.get("SAMLRequest"); + + if (samlRequest) { + console.log("SAML AuthnRequest λΆμ:"); + console.log("1οΈβ£ μλ³Έ URL:", authorizeUrl); + console.log( + "2οΈβ£ URL λμ½λ©λ SAMLRequest:", + decodeURIComponent(samlRequest) + ); + + try { + // Base64 λμ½λ© + const base64DecodedBuffer = Buffer.from( + decodeURIComponent(samlRequest), + "base64" + ); + const base64DecodedString = base64DecodedBuffer.toString("utf-8"); + + // XMLμΈμ§ νμΈ (XMLμ '<'λ‘ μμν¨) + if (base64DecodedString.trim().startsWith("<")) { + console.log("Base64 λμ½λ©λ XML (μμΆ μμ):"); + console.log("βββββββββββββββββββββββββββββββββββ"); + console.log(base64DecodedString); + console.log("βββββββββββββββββββββββββββββββββββ"); + + // XML ꡬ쑰 λΆμ + const xmlLines = base64DecodedString + .split("\n") + .filter((line) => line.trim()); + console.log("XML ꡬ쑰 μμ½:"); + xmlLines.forEach((line, index) => { + const trimmed = line.trim(); + if ( + trimmed.includes("<saml") || + trimmed.includes("<samlp") || + trimmed.includes("ID=") || + trimmed.includes("Destination=") + ) { + console.log(` ${index + 1}: ${trimmed}`); + } + }); + } else { + // XMLμ΄ μλλ©΄ Deflate μμΆλ κ²μΌλ‘ κ°μ£Ό + console.log( + "3οΈβ£ μμΆλ λ°μ΄λ리 λ°μ΄ν° κ°μ§, Deflate μμΆ ν΄μ μλ..." + ); + + try { + const zlib = await import("zlib"); + const decompressed = zlib + .inflateRawSync(base64DecodedBuffer) + .toString("utf-8"); + console.log("Deflate μμΆ ν΄μ λ XML:"); + console.log("βββββββββββββββββββββββββββββββββββ"); + console.log(decompressed); + console.log("βββββββββββββββββββββββββββββββββββ"); + + // XML ꡬ쑰 λΆμ + const xmlLines = decompressed + .split("\n") + .filter((line: string) => line.trim()); + console.log("XML ꡬ쑰 μμ½:"); + xmlLines.forEach((line: string, index: number) => { + const trimmed = line.trim(); + if ( + trimmed.includes("<saml") || + trimmed.includes("<samlp") || + trimmed.includes("ID=") || + trimmed.includes("Destination=") || + trimmed.includes("Issuer>") || + trimmed.includes("AssertionConsumerServiceURL=") + ) { + console.log(` ${index + 1}: ${trimmed}`); + } + }); + + // μ€μν μ 보 μΆμΆ + const idMatch = decompressed.match(/ID="([^"]+)"/); + const destinationMatch = decompressed.match( + /Destination="([^"]+)"/ + ); + const issuerMatch = decompressed.match( + /<saml:Issuer[^>]*>([^<]+)<\/saml:Issuer>/ + ); + const acsMatch = decompressed.match( + /AssertionConsumerServiceURL="([^"]+)"/ + ); + + console.log("μΆμΆλ ν΅μ¬ μ 보:"); + console.log(` Request ID: ${idMatch ? idMatch[1] : "μμ"}`); + console.log( + ` Destination: ${ + destinationMatch ? destinationMatch[1] : "μμ" + }` + ); + console.log( + ` Issuer: ${issuerMatch ? issuerMatch[1] : "μμ"}` + ); + console.log( + ` Callback URL: ${acsMatch ? acsMatch[1] : "μμ"}` + ); + } catch (inflateError) { + console.log("β Deflate μμΆ ν΄μ μ€ν¨:", (inflateError as Error).message); + console.log( + " μλ³Έ λ°μ΄λ리 λ°μ΄ν° (hex):", + base64DecodedBuffer.toString("hex").substring(0, 100) + "..." + ); + } + } + } catch (decodeError) { + console.log("β Base64 λμ½λ© μ€ν¨:", (decodeError as Error).message); + } + } + } catch (analysisError) { + console.log("β οΈ SAML AuthnRequest λΆμ μ€ μ€λ₯:", (analysisError as Error).message); + } + + console.log("β
SAML AuthnRequest URL generated:", { + url: authorizeUrl.substring(0, 100) + "...", + fullUrlLength: authorizeUrl.length, + processingTime: `${endTime - startTime}ms`, + timestamp: new Date().toISOString(), + }); + + return authorizeUrl; + } catch (error) { + console.error("π₯ Failed to create SAML AuthnRequest:", { + error: error instanceof Error ? error.message : "Unknown error", + stack: error instanceof Error ? error.stack : undefined, + timestamp: new Date().toISOString(), + }); + throw error; + } +} + +// SAML Response κ²μ¦ λ° νμ± (μλ² μ‘μ
) +export async function validateSAMLResponse( + samlResponse: string +): Promise<SAMLProfile> { + "use server"; + + console.log("π Starting SAML Response validation..."); + console.log("π SAML Response info:", { + responseLength: samlResponse.length, + firstChars: samlResponse.substring(0, 50) + "...", + isBase64: /^[A-Za-z0-9+/]*={0,2}$/.test(samlResponse), + timestamp: new Date().toISOString(), + }); + + // Mock IdP λͺ¨λ μ²΄ν¬ + if (process.env.SAML_MOCKING_IDP === 'true') { + debugMock("Mock IdP mode - returning mock SAML profile"); + return createMockSAMLProfile(samlResponse); + } + + // μ€μ SAML κ²μ¦ μν (κΈ°λ³Έκ°) + console.log( + "π Using Real SAML validation (SAML_MOCKING_IDP=false or not set)" + ); + + try { + console.log("βοΈ Creating SAML instance for validation..."); + const saml = new SAML(createSAMLConfig()); + console.log("β
SAML instance created, starting validation..."); + + const startTime = Date.now(); + const result = await saml.validatePostResponseAsync({ + SAMLResponse: samlResponse, + }); + const endTime = Date.now(); + + // node-saml λΌμ΄λΈλ¬λ¦¬λ { profile, loggedOut } ννλ‘ λ°ν + const profile = result.profile; + if (!profile) { + throw new Error("No profile returned from SAML validation"); + } + + // SAMLProfile ννλ‘ λ³ν (νμ
μμ μ± ν보) + const samlProfile: SAMLProfile = { + nameID: profile.nameID as string | undefined, + nameIDFormat: profile.nameIDFormat as string | undefined, + attributes: profile.attributes as Record<string, string | string[]> | undefined, + }; + + console.log("β
Real SAML Profile validated successfully:", { + nameID: samlProfile.nameID, + nameIDFormat: samlProfile.nameIDFormat, + attributeCount: Object.keys(samlProfile.attributes || {}).length, + attributes: Object.keys(samlProfile.attributes || {}), + processingTime: `${endTime - startTime}ms`, + timestamp: new Date().toISOString(), + }); + + return samlProfile; + } catch (error) { + console.error("β Real SAML validation error:", { + error: error instanceof Error ? error.message : "Unknown error", + stack: error instanceof Error ? error.stack : undefined, + samlResponseLength: samlResponse.length, + timestamp: new Date().toISOString(), + }); + throw new Error( + `SAML validation failed: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } +} + +// SAML Profileμ User κ°μ²΄λ‘ λ³ν (sync ν¨μ) +export function mapSAMLProfileToUser(profile: SAMLProfile): SAMLUser { + console.log("π Mapping SAML profile to user:", { + nameID: profile.nameID, + attributes: profile.attributes, + }); + + // SAML attributesλ λ¬Έμμ΄ λλ λ°°μ΄ ννμΌ μ μμ + const extractAttributeValue = (key: string): string | undefined => { + const value = profile.attributes?.[key]; + if (Array.isArray(value)) { + return value.length > 0 ? value[0] : undefined; + } + return typeof value === 'string' ? value : undefined; + }; + + // κΈ°λ³Έμ μΌλ‘ nameIDλ₯Ό μ¬μ©νκ±°λ attributesμμ μΆμΆ + const id = profile.nameID || extractAttributeValue('id') || extractAttributeValue('sub'); + const email = extractAttributeValue('email') || extractAttributeValue('emailAddress'); + const name = extractAttributeValue('name') || extractAttributeValue('displayName') || extractAttributeValue('cn'); + + // νμ νλ κ²μ¦ + if (!id) { + throw new Error('SAML profile missing required field: id (nameID)'); + } + if (!email) { + throw new Error('SAML profile missing required field: email'); + } + if (!name) { + throw new Error('SAML profile missing required field: name'); + } + + // UTF-8 λ¬Έμμ΄ μ κ·ν λ° κ²μ¦ + const normalizedName = name.normalize("NFC").trim(); + + // νκΈμ΄ κΉ¨μ§ κ²½μ° κ°μ§ λ° λ‘κ·Έ + const hasInvalidChars = /[\uFFFD\x00-\x1F\x7F-\x9F]/.test(normalizedName); + if (hasInvalidChars) { + console.warn("β οΈ Invalid UTF-8 characters detected in name:", { + originalName: name, + normalizedName, + charCodes: [...normalizedName].map((c) => c.charCodeAt(0)), + hexDump: [...normalizedName] + .map((c) => "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0")) + .join(""), + }); + } + + // νμ¬ μ 보λ SSO λ‘κ·ΈμΈ μ μμ (evcp λλ©μΈ) + const companyId = undefined; + const techCompanyId = undefined; + const domain = 'evcp'; + + const user: SAMLUser = { + id, + email, + name: normalizedName, + companyId, + techCompanyId, + domain, + }; + + console.log("π€ Mapped user object:", JSON.stringify(user)); + + return user; +} + +// Mock SAML νλ‘μ° μμ± (ν
μ€νΈμ©) +function createMockSAMLFlow(relayState?: string): string { + debugMock("Creating mock SAML flow...", { relayState }); + + // Mock λͺ¨λμμλ Mock IdP μλν¬μΈνΈλ‘ 리λ€μ΄λ μ
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; + let mockIdpUrl = `${baseUrl}/api/auth/saml/mock-idp`; + + // RelayStateκ° μμΌλ©΄ URL νλΌλ―Έν°λ‘ μ λ¬ + if (relayState) { + mockIdpUrl += `?RelayState=${encodeURIComponent(relayState)}`; + } + + debugMock("Mock SAML Flow - redirecting to Mock IdP:", mockIdpUrl); + + return mockIdpUrl; +} + +// Mock SAML Profile μμ± (ν
μ€νΈμ©) +function createMockSAMLProfile(samlResponse: string): SAMLProfile { + console.log("π Creating mock SAML profile from response..."); + + try { + // SAML Responseκ° μ°λ¦¬κ° μμ±ν MockμΈμ§ νμΈ + const decodedXML = Buffer.from(samlResponse, 'base64').toString('utf-8'); + const isMockResponse = decodedXML.includes('MockIdP'); + + if (!isMockResponse) { + console.warn("β οΈ Mock mode enabled but received non-mock SAML Response"); + } + + console.log("π Mock SAML XML preview:", decodedXML.substring(0, 200) + "..."); + } catch (error) { + console.warn("β οΈ Could not decode SAML Response for mock analysis:", (error as Error).message); + } + + // Mock SAML Profile λ°ν (μ€μ SAML Responseμ μΌμΉνλλ‘ λ¬Έμμ΄ νν) + const mockProfile: SAMLProfile = { + nameID: "testuser@samsung.com", + nameIDFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress", + attributes: { + email: "testuser@samsung.com", + name: "ν
μ€νΈ μ¬μ©μ", + displayName: "Test User Samsung", + // μΆκ° ν
μ€νΈ μμ±λ€ + department: "κ°λ°ν", + employeeId: "TEST001", + mobile: "010-1234-5678" + } + }; + + console.log("π Mock SAML Profile created:", { + nameID: mockProfile.nameID, + nameIDFormat: mockProfile.nameIDFormat, + attributeCount: Object.keys(mockProfile.attributes || {}).length, + attributes: Object.keys(mockProfile.attributes || {}), + timestamp: new Date().toISOString(), + }); + + return mockProfile; +} + +// SAML λ‘κ·Έμμ URL μμ± (μλ² μ‘μ
) +// λ‘κ·Έμμ μ§μ μν¨. μΌλ¨ κ΅¬μ‘°λ§ μ μ¬νκ² μμ±ν΄λ . +export async function createLogoutRequest(nameID: string): Promise<string> { + "use server"; + + const saml = new SAML(createSAMLConfig()); + // Profile κ°μ²΄ ννλ‘ μ λ¬ + const profile = { nameID }; + return await saml.getLogoutUrlAsync( + profile, + "", // RelayState + { + nameIDFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + } + ); +} |
